Go 的 signal 包-监听信号
什么是信号
信号(Signal)是 Linux, 类 Unix 和其它 POSIX 兼容的操作系统中用来进程间通讯的一种方式。对于 Linux 系统来说,信号就是软中断,用来通知进程发生了异步事件。
信号 | 值 | 动作 | 说明 |
---|---|---|---|
SIGHUP | 1 | Term | 终端控制进程结束(终端连接断开) |
SIGINT | 2 | Term | 用户发送INTR字符(Ctrl+C)触发 |
SIGQUIT | 3 | Core | 用户发送QUIT字符(Ctrl+/)触发 |
SIGILL | 4 | Core | 非法指令(程序错误、试图执行数据段、栈溢出等) |
SIGABRT | 6 | Core | 调用abort函数触发 |
SIGFPE | 8 | Core | 算术运行错误(浮点运算错误、除数为零等) |
SIGKILL | 9 | Term | 无条件结束程序(不能被捕获、阻塞或忽略) |
SIGSEGV | 11 | Core | 无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作) |
SIGPIPE | 13 | Term | 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作) |
SIGALRM | 14 | Term | 时钟定时信号 |
SIGTERM | 15 | Term | 结束程序(可以被捕获、阻塞或忽略) |
SIGUSR1 | 30,10,16 | Term | 用户保留 |
SIGUSR2 | 31,12,17 | Term | 用户保留 |
SIGCHLD | 20,17,18 | Ign | 子进程结束(由父进程接收) |
SIGCONT | 19,18,25 | Cont | 继续执行已经停止的进程(不能被阻塞) |
SIGSTOP | 17,19,23 | Stop | 停止进程(不能被捕获、阻塞或忽略) |
SIGTSTP | 18,20,24 | Stop | 停止进程(可以被捕获、阻塞或忽略) |
SIGTTIN | 21,21,26 | Stop | 后台程序从终端中读取数据时触发 |
SIGTTOU | 22,22,27 | Stop | 后台程序向终端中写数据时触发 |
当信号发送到某个进程中时,操作系统会中断该进程的正常流程,并进入相应的信号处理函数执行操作,完成后再回到中断的地方继续执行。
有时候我们想在 Go 程序中处理 Signal 信号,比如收到 SIGTERM 信号后优雅的关闭程序,以及 goroutine 结束通知等。Go 语言提供了对信号处理的包(os/signal)。
Go 中对信号的处理主要使用 os/signal 包中的两个方法:
- notify 方法用来监听收到的信号;
- stop 方法用来取消监听。
如下第一个参数是一个通道,参数 c 是调用者的信号接收通道,Notify 可将进入的信号转到 c。sig 参数为需要转发的信号类型,若不指定,所有进入的信号都将会转到 c。
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
信号不会阻塞式的发给 c,所以调用者需确保 c 有足够的缓冲空间,以应对指定信号的高频发送。对于用于通知仅一个信号值的通道,缓冲大小为 1 即可。
参看如下代码,当使用 Ctrl+C 时,c 会接收到中断信号,程序会在打印 “program interrupted” 语句后退出。
func main() {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt)
<-c
log.Fatal("program interrupted")
}
Notify 示例
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
// 创建一个os.Signal channel
sigs := make(chan os.Signal, 1)
//创建一个bool channel
done := make(chan bool, 1)
//注册要接收的信号,syscall.SIGINT:接收ctrl+c ,syscall.SIGTERM:程序退出
//信号没有信号参数表示接收所有的信号
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
//此goroutine为执行阻塞接收信号。一旦有了它,它就会打印出来。
//然后通知程序可以完成。
go func() {
sig := <-sigs
fmt.Println(sig)
done <- true
}()
//程序将在此处等待,直到它预期信号(如Goroutine所示)
//在“done”上发送一个值,然后退出。
fmt.Println("awaiting signal")
<-done
fmt.Println("exiting")
}
取消监听 stop
func main() {
// 创建一个os.Signal channel
sigs := make(chan os.Signal, 1)
//创建一个bool channel
done := make(chan bool, 1)
//注册要接收的信号,syscall.SIGINT:接收ctrl+c ,syscall.SIGTERM:程序退出
//信号没有信号参数表示接收所有的信号
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
//此goroutine为执行阻塞接收信号。一旦有了它,它就会打印出来。
//然后通知程序可以完成。
go func() {
sig := <-sigs
fmt.Println(sig)
done <- true
}()
//不允许继续往sigs中存入内容
signal.Stop(sigs)
//程序将在此处等待,直到它预期信号(如Goroutine所示)
//在“done”上发送一个值,然后退出。
fmt.Println("awaiting signal")
<-done
fmt.Println("exiting")
}
Server 优雅的终止
采用常规方式启动一个 Golang http 服务时,若服务被意外终止或中断,即未等待服务对现有请求连接处理并正常返回且亦未对服务停止前作一些必要的处理工作,这样即会造成服务硬终止。这种方式不是很优雅。
参看如下代码,该 http 服务请求路径为根路径,请求该路径,其会在 2s 后返回 hello。
var addr = flag.String("server addr", ":8080", "server address")
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
fmt.Fprintln(w, "hello")
})
http.ListenAndServe(*addr, nil)
}
若服务启动后,请求 http://localhost:8080/ ,然后使用 Ctrl+C 立即中断服务,服务即会立即退出(exit status 2),请求未正常返回(ERR_CONNECTION_REFUSED),连接即马上断了。
接下来介绍使用 http.Server 的 Shutdown 方法结合 signal.Notify 来优雅的终止服务。
Golang http.Server 结构体有一个终止服务的方法 Shutdown,使用 Shutdown 可以优雅的终止服务,其不会中断活跃连接。其工作过程为:首先关闭所有开启的监听器,然后关闭所有闲置连接,最后等待活跃的连接均闲置了才终止服务。
如下代码:
- 创建一个 http.Server 实例,指定端口与 Handler。
- 声明一个 processed chan,其用来保证服务优雅的终止后再退出主 goroutine。
- 新启一个 goroutine,其会监听 os.Interrupt 信号,一旦服务被中断即调用服务的 Shutdown 方法,确保活跃连接的正常返回(本代码使用的 Context 超时时间为 3s,大于服务 Handler 的处理时间,所以不会超时)。
- 处理完成后,关闭 processed 通道,最后主 goroutine 退出。
var addr = flag.String("server addr", ":8080", "server address")
func main() {
// handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
fmt.Fprintln(w, "hello")
})
// server
srv := http.Server{
Addr: *addr,
Handler: handler,
}
// make sure idle connections returned
processed := make(chan struct{})
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); nil != err {
log.Fatalf("server shutdown failed, err: %v\n", err)
}
log.Println("server gracefully shutdown")
close(processed)
}()
// serve
err := srv.ListenAndServe()
if http.ErrServerClosed != err {
log.Fatalf("server not gracefully shutdown, err :%v\n", err)
}
// waiting for goroutine above processed
<-processed
}